<#assign object = doc.object>
package ${object.package}.service;

import java.util.*;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.logging.Level;
import java.util.logging.Logger;
import redora.rdo.api.*;
import redora.rdo.exceptions.*;
import redora.rdo.impl.*;
import ${object.package}.model.*;
import ${object.package}.model.fields.*;
import com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException;

<#list doc["/object/attributes/enum[@scope='global']"] as att>
import ${object.package}.model.enums.${att.@class};
</#list>
<#if object.trashcan[0]?? && object.trashcan == "true">
import redora.rdo.configuration.rdo.service.TrashService;
</#if>
/**
* You can work with the pojo's using this service. For example if you create command line J2SE functionality
* or server side logic.<br>
* If you want to use your data in a web application, you also might want to look at {@link ${object.name}ServiceJSON}
* and {@link ${object.package}.service.http.${object.name}Servlet}.<br>
* Whenever business rule logic needs to be checked, it will go through here. For example a web user wants to persist
* some data. He will post this for example as JSON to {@link ${object.package}.service.http.${object.name}Servlet}
* which in turn invokes {@link ${object.name}ServiceJSON} which will serialize the JSON stream into a pojo
* and then will invoke the persist method of this service.
* @author Redora
*/
public class ${object.name}Service extends ServiceBase {
	private Logger l = Logger.getLogger("${object.package}.service.${object.name}Service");
	
	private ${object.name}Util util;

	/**
	* Don't use this directly but use the ServiceFactory instead: <br />
	* ${object.name}Service() myService = ServiceFactory.${object.name?uncap_first}();
	* <br />
	* Initializes ${object.name}Service and connects to the database.
	* The connection will be cached until you issue close(). 
	* @throws ConnectException When a connection cannot be established.
	* @see ${object.package}.service.ServiceFactory
	*/
	public ${object.name}Service() throws ConnectException {
		super();
		util = new ${object.name}Util();
	}

	/** 
	* @param scope (Mandatory) The allowed fetchscope: FetchScope.Table , FetchScope.Form.
	* @return Null, or the desired ${object.name} 
	*/
	public ${object.name} findById(Long id, FetchScope scope) throws ConnectException, QueryException, CopyException {
		${object.name} retVal = null;
	    if (!(scope == FetchScope.Form || scope == FetchScope.Table)) {
    	  throw new CopyException("Illegal scope for this finder: " + scope);
    	}
	    String SQL = scope == FetchScope.Table ? ${object.name}SQL.FIND_BY_ID_TABLE : ${object.name}SQL.FIND_BY_ID_FORM;
		try {
			makePreparedStatement(SQL);
			ps.setLong(1, id.longValue());
			rs = ps.executeQuery();
			if (rs.next())
				retVal = util.copy(rs, 0, scope);
			rs.close();
			rs = null;
		} catch(SQLException e) {
			l.log(Level.SEVERE, "Failed to run query " + SQL, e);
			throw new QueryException("Failed to run query: " + SQL, e);
	  	}
		return retVal;
	}

  	/**
  	* Finds records by custom SQL. Custom queries should be defined in the queries section in the model.
  	* @param params (Optional) List of parameters. When omitted it is assumed there are no parameters.
  	* @param scope (Mandatory) The allowed fetchscope: FetchScope.Table, FetchScope.Form, FetchScope.List, FetchScope.Lazy.
  	*/
  	public List<${object.name}> find(String SQL, List<Object> params, FetchScope scope) throws ConnectException, QueryException, CopyException {
    	List<${object.name}> retVal = new ArrayList<${object.name}>();
    	try {
      		makePreparedStatement(SQL);
      		int position = 1;
      		if (params != null) {
        		for (Object param: params) {
          			if (param instanceof Integer) {
            			ps.setInt(position++, (Integer)param);
          			} else if (param instanceof Long) {
            			ps.setLong(position++, (Long)param);
          			} else if (param instanceof String) {
            			ps.setString(position++, (String)param);
          			} else if (param instanceof Double) {
            			ps.setDouble(position++, (Double)param);
          			} 
        		}
      		}

      		rs = ps.executeQuery();
      		while (rs.next()) {
        		retVal.add(util.copy(rs, 0, scope));
      		}
      		rs.close();
      		rs = null;
    	} catch(SQLException e) {
      		l.log(Level.SEVERE, "Failed to run query: " + SQL, e);
      		throw new QueryException("Failed to run query: " + SQL, e);
    	}
    
    	return retVal;
  	}
	
	/**
	* Finds All records
	* @param scope (Mandatory) The allowed fetchscope: FetchScope.Table, FetchScope.List.
	*/
	public List<${object.name}> findAll(FetchScope scope) throws ConnectException, QueryException, CopyException {
		List<${object.name}> retVal = new ArrayList<${object.name}>();
	    if (!(scope == FetchScope.List || scope == FetchScope.Table)) {
    	  throw new CopyException("Illegal scope for this finder: " + scope);
    	}
	    String SQL = scope == FetchScope.Table ? ${object.name}SQL.FIND_ALL_TABLE : ${object.name}SQL.FIND_ALL_LIST;
		try {
			makePreparedStatement(SQL);
			rs = ps.executeQuery();
			while (rs.next())
				retVal.add(util.copy(rs, 0, scope));
			rs.close();
			rs = null;
	  	} catch(SQLException e) {
	  		if (scope == FetchScope.List) {
				l.log(Level.SEVERE, "Failed to run query " + SQL, e);
				throw new QueryException("Failed to run query: " + SQL, e);
			} else {
				l.log(Level.SEVERE, "Failed to run query " + SQL, e);
				throw new QueryException("Failed to run query: " + SQL, e);
			}
	  	}
		return retVal;
	}
	
<#list doc["/object/attributes/set[@multiplicity='n-to-m']"] as att>
  	/**
  	* Finds ${att.@plural?cap_first} based on their (n-to-m) relation with ${att.@class}.
  	* @param scope (Mandatory) The allowed fetchscope: FetchScope.Table, FetchScope.List.
  	*/
  	public List<${object.name}> findBy${att.@class}Id (Long id, FetchScope scope) throws ConnectException, CopyException, QueryException {
    	List<${object.name}> retVal = new ArrayList<${object.name}>();
    
    	if (!(scope == FetchScope.List || scope == FetchScope.Table)) {
      		throw new CopyException("Illegal scope for this finder: " + scope);
    	}
    	String SQL = scope == FetchScope.Table ? ${object.name}SQL.FIND_${object.name?upper_case}_BY_${att.@class?upper_case}ID_TABLE : ${object.name}SQL.FIND_${object.name?upper_case}_BY_${att.@class?upper_case}ID_LIST;
    	try {
     		makePreparedStatement(SQL);
      	ps.setLong(1, id);
      	rs = ps.executeQuery();
      	while (rs.next())
        	retVal.add(util.copy(rs, 0, scope));
      	rs.close();
      	rs = null;
    	} catch(SQLException e) {
      		l.log(Level.SEVERE, "Failed to run query " + SQL, e);
      		throw new QueryException("Failed to run query: " + SQL, e);
    	}
    	return retVal;
  	}
</#list>

<#list object.attributes?children as att>
    <#if att?node_name == "long" && att.@name?ends_with("Id")>
        <@relation att=att />
    /**
	* @param scope (Mandatory) The allowed fetchscope: FetchScope.Table, FetchScope.List.
	*/
  	public List<${object.name}> findBy${relation_name?cap_first}(Long id, FetchScope scope) throws ConnectException, QueryException, CopyException {
    	if (!(scope == FetchScope.List || scope == FetchScope.Table)) {
      		throw new CopyException("Illegal scope: " + scope);
    	}
    	List<${object.name}> retVal = new ArrayList<${object.name}>();
	    String SQL = scope == FetchScope.Table ? ${object.name}SQL.FIND_BY_${relation_name?upper_case}_TABLE : ${object.name}SQL.FIND_BY_${relation_name?upper_case}_LIST;
	    try {
    		makePreparedStatement(SQL);
	      	ps.setLong(1, id.longValue());
	      	rs = ps.executeQuery();
	      	while (rs.next()) {
	        	retVal.add(util.copy(rs, 0, scope));
	      	}
      	rs.close();
      	rs = null;
	    } catch(SQLException e) {
	      l.log(Level.SEVERE, "Failed to run query " + SQL, e);
	      throw new QueryException("Failed to run query: " + SQL, e);
	    }
    
    	return retVal;
  	}

  	/** Deletes all ${object.name}s without checking on business rules. This is usually invoked by the related parent which has already checked business rule violations. */
  	public void deleteBy${relation_name?cap_first}(Long id) throws ConnectException, QueryException {
    	try {
      	makePreparedStatement(${object.name}SQL.DELETE_BY_${relation_name?upper_case});
      	ps.setLong(1, id.longValue());
      	ps.execute();
    	} catch(SQLException e) {
      		l.log(Level.SEVERE, "Failed to run delete query " + ${object.name}SQL.DELETE_BY_${relation_name?upper_case}, e);
      	throw new QueryException("Failed to run query: " + ${object.name}SQL.DELETE_BY_${relation_name?upper_case}, e);
    	}
  	}

        <#if object.trashcan[0]?? && object.trashcan == "true">
  	/** Trashes all ${object.name}s without checking on business rules. This is usually invoked by the related parent which has already checked business rule violations. */
  	public void trashBy${relation_name?cap_first}(Long id, boolean undo) throws ConnectException, QueryException {
    	try {
      	makePreparedStatement(${object.name}SQL.TRASH_BY_${relation_name?upper_case});
      	ps.setBoolean(1, !undo);
      	ps.setLong(2, id.longValue());
      	ps.execute();
    	} catch(SQLException e) {
      		l.log(Level.SEVERE, "Failed to run delete query " + ${object.name}SQL.TRASH_BY_${relation_name?upper_case}, e);
      	throw new QueryException("Failed to run query: " + ${object.name}SQL.TRASH_BY_${relation_name?upper_case}, e);
    	}
  	}
        </#if>
    </#if>
</#list>

<#list object.attributes.enum as att>
	/**
	* @param scope (Mandatory) The allowed fetchscope: FetchScope.Table, FetchScope.List.
	*/
	public List<${object.name}> findBy${att.@fieldName?cap_first}(<#if !att.@scope[0]?? || att.@scope == "local">${object.name}.</#if>${att.@className} ${att.@fieldName}, FetchScope scope) throws ConnectException, QueryException, CopyException {
		if (!(scope == FetchScope.List || scope == FetchScope.Table)) {
      		throw new CopyException("Illegal scope: " + scope);
    	}
    	List<${object.name}> retVal = new ArrayList<${object.name}>();
    	String SQL = scope == FetchScope.Table ? ${object.name}SQL.FIND_BY_${att.@fieldName?upper_case}_TABLE : ${object.name}SQL.FIND_BY_${att.@fieldName?upper_case}_LIST;
    	try {
			makePreparedStatement(SQL);
			ps.setString(1, ${att.@fieldName}.name());
			rs = ps.executeQuery();
			while (rs.next())
				retVal.add(util.copy(rs, 0, scope));
			rs.close();
			rs = null;
		} catch(SQLException e) {
			l.log(Level.SEVERE, "Failed to run query " + SQL, e);
			throw new QueryException("Failed to run query: " + SQL, e);
	  	}
		return retVal;
	}
</#list>

<#list doc["/object/attributes/*[@finder='true']"] as att>
	/**
	* @param scope (Mandatory) The allowed fetchscope: FetchScope.Table, FetchScope.List.
	*/
	public List<${object.name}> findBy${att.@fieldName?cap_first}(${att.@className} finder, FetchScope scope) throws ConnectException, QueryException, CopyException {
		if (!(scope == FetchScope.List || scope == FetchScope.Table)) {
      		throw new CopyException("Illegal scope: " + scope);
    	}
    	List<${object.name}> retVal = new ArrayList<${object.name}>();
    	String SQL = scope == FetchScope.Table ? ${object.name}SQL.FIND_BY_${att.@fieldName?upper_case}_TABLE : ${object.name}SQL.FIND_BY_${att.@fieldName?upper_case}_LIST;
    	try {
			makePreparedStatement(SQL);
			<#if att?node_name == "long">
			ps.setLong(1, finder.longValue());
			<#elseif att?node_name == "integer">
			ps.setInt(1, finder.intValue());
			<#elseif att?node_name == "string">
			ps.setString(1, finder);
			<#elseif att?node_name == "enum">
			ps.setString(1, finder.name());
			<#elseif att?node_name == "date">
			ps.setDate(1, new java.sql.Date(finder.getTime()));
			<#elseif att?node_name == "double">
			ps.setDouble(1, finder.doubleValue());
			</#if>
			rs = ps.executeQuery();
			while (rs.next())
				retVal.add(util.copy(rs, 0, scope));
			rs.close();
			rs = null;
		} catch(SQLException e) {
			l.log(Level.SEVERE, "Failed to run query " + SQL, e);
			throw new QueryException("Failed to run query: " + SQL, e);
	  	}
		return retVal;
	}
</#list>

<#if doc["/object/attributes/*[@lazy='true']"][0]??>
	public Map<String, Object> fetchLazy(Long id) throws ConnectException, QueryException, CopyException {
		Map<String, Object> map = null;
		try {
			makePreparedStatement(${object.name}SQL.FETCH_LAZY_BY_ID);
			ps.setLong(1,id);
			rs = ps.executeQuery();
			if (rs.next()) 
				map = util.copyLazy(rs,0);
			rs.close();
			rs = null;
		} catch(SQLException e) {
			l.log(Level.SEVERE, "Failed to run query " + ${object.name}SQL.FETCH_LAZY_BY_ID, e);
			throw new QueryException("Failed to run query: " + ${object.name}SQL.FETCH_LAZY_BY_ID, e);
	  	}
		return map;
	}
</#if>

	/**
	* Inserts or updates ${object.name}. Collections associated to ${object.name} are persisted as well.
	**/
	public Set<BusinessRuleViolation> persist(${object.name} pojo) throws ConnectException, PersistException, BusinessRuleViolationException, QueryException<#if object.attributes.set[0]??>, CopyException, LazyException</#if> {
		if (!evaluateChange(pojo))
			return new HashSet<BusinessRuleViolation>();
		Set<BusinessRuleViolation> br = checkBR(pojo, pojo.isNew ? BusinessRuleViolation.Action.Insert : BusinessRuleViolation.Action.Update);
		if (br.size() != 0)
			return br;
		return persist(pojo, false);
	}

	/**
	* Inserts or updates a collection of ${object.name}. 
	* Collections associated to ${object.name} are persisted as well.
	* JDBC batch update is used to improve performance.
	**/
	public Set<BusinessRuleViolation> persist(List<${object.name}> pojos) throws ConnectException, PersistException, BusinessRuleViolationException, QueryException<#if object.attributes.set[0]??>, CopyException, LazyException</#if> {
		boolean unChanged = true;
		for (${object.name} pojo : pojos) {
			if (evaluateChange(pojo)) {
				unChanged = false;
				break;	
			}
		}
		if (unChanged) {
			return new HashSet<BusinessRuleViolation>();
		}

		Set<BusinessRuleViolation> br = new HashSet<BusinessRuleViolation>();
		for (${object.name} pojo : pojos) {
			br.addAll(checkBR(pojo, pojo.isNew ? BusinessRuleViolation.Action.Insert : BusinessRuleViolation.Action.Update));
		}
		
		if (br.size() != 0) {
			return br;
		}

		for (${object.name} pojo : pojos) {
			br.addAll(persist(pojo, true));
		}

		try {
			ps.executeBatch();
			ResultSet _r = ps.getGeneratedKeys(); 
			while (_r.next())
				for (${object.name} pojo : pojos)
					if (pojo.getId() == null)
						pojo.id = _r.getLong(1);
			_r.close();
		} catch(SQLException e) {
			l.log(Level.SEVERE, "Failed to perform batch update, i was trying to update a list of " + pojos.size() + " ${object.name}s", e);
			throw new PersistException("Failed to perform batch update, i was trying to update a list of " + pojos.size() + " ${object.name}s", e);		
		}
		return br;
	}
	
  	protected Set<BusinessRuleViolation> checkBR(${object.name} pojo, BusinessRuleViolation.Action when) throws ConnectException, BusinessRuleViolationException<#if object.attributes.set[0]??>, QueryException, CopyException</#if> {
    	Set<BusinessRuleViolation> retVal = new HashSet<BusinessRuleViolation>();
    
    	${object.name}BusinessRules br = new ${object.name}BusinessRules(util); 
    	if (when == BusinessRuleViolation.Action.Delete) {
      		retVal.addAll(br.onDelete(pojo));
    	} else if (when == BusinessRuleViolation.Action.Insert) {
      		retVal.addAll(br.onInsert(pojo));
    	} else {
      		retVal.addAll(br.onUpdate(pojo));
		}

<#list object.attributes.set as att>
    	//Operations on ${att.@multiplicity} relation with ${att.@class}:
    	${att.@class}BusinessRules br${att.@class} = new ${att.@class}BusinessRules(new ${att.@class}Util());
    	for (${att.@class} child : pojo.get${att.@plural?cap_first}()) {
      		if (when == BusinessRuleViolation.Action.Delete) {
    <#if !att.@cascadeDelete[0]?? || att.@cascadeDelete == "true">
        		retVal.addAll(br${att.@class}.onDelete(child));
    <#else>
        		l.log(Level.FINE, "Ignoring business rule check because cascade delete is turned of");
    </#if>
      		} else {
    <#if att.@multiplicity == "1-to-n">
        		if (when == BusinessRuleViolation.Action.Insert) {
          			child.set${object.name}Id(-1L); //make sure the not-null constraint is not violated, it will be reset on persist()
        		} else {
          			child.set${object.name}Id(pojo.getId());
        		}
    </#if>
        		if (child.isNew) {
          			retVal.addAll(br${att.@class}.onInsert(child));
        		} else {
          			retVal.addAll(br${att.@class}.onUpdate(child));
        		}
      		}
		}
    <#if (!att.@multiplicity[0]?? || att.@multiplicity == "1-to-n") && (!att.@cascadeDelete[0]?? || att.@cascadeDelete == "true")>
    	for (${att.@class} delete : pojo.get${att.@plural?cap_first}().getRemovedObjects())
      		retVal.addAll(br${att.@class}.onDelete(delete));
    </#if>
</#list>
		
    	return retVal;
  	}

	/**
	* @return true when ${object.name} or any related children are changed
	*/
	protected static boolean evaluateChange(${object.name} pojo) <#if object.attributes.set[0]??>throws ConnectException, QueryException, CopyException</#if> {
		if (pojo.isDirty())
			return true;
		<#list object.attributes.set as att>
		if (pojo.get${att.@plural?cap_first}().isDirty())
			return true;
		</#list>
		return false;
	}
	
<#list doc["/object/attributes/set[@multiplicity='n-to-m']"] as att>
	<@relation_table att=att />
  	/**
  	* Removes the relationship with ${att.@class} by deleting a record in the ${relation_table_name} table. 
  	*/
  	protected void deleteRelationWith${att.@class}(Long ${object.name?uncap_first}Id, Long ${att.@class?uncap_first}Id, boolean asBatch)
      	throws ConnectException, PersistException {
    	try {
      		makePreparedStatement(${object.name}SQL.DELETE_${relation_table_name?upper_case}_RELATION);
      		ps.setLong(1, ${object.name?uncap_first}Id);
      		ps.setLong(2, ${att.@class?uncap_first}Id);
      		if(!asBatch) {
        		ps.execute();
      		} else {
        		ps.addBatch();
      		}
    	} catch (SQLException e) {
      		l.log(Level.SEVERE, "Failed to perform delete: "
        		+ ${object.name}SQL.DELETE_${relation_table_name?upper_case}_RELATION, e);
      		throw new PersistException("Failed to perform delete: "
        		+ ${object.name}SQL.DELETE_${relation_table_name?upper_case}_RELATION, e);
    	}
  	}
    
	/**
	* Removes the all relationships with ${att.@class} by deleting the records in the ${relation_table_name} table. 
	*/
  	protected void deleteAllRelationsWith${att.@class}(Long ${object.name?uncap_first}Id, boolean asBatch)
      	throws ConnectException, PersistException {
    	try {
      		makePreparedStatement(${object.name}SQL.DELETE_${relation_table_name?upper_case}_RELATION_BY_${object.name?upper_case}ID);
      		ps.setLong(1, ${object.name?uncap_first}Id);
      		if (!asBatch) {
        		ps.execute();
      		} else {
        		ps.addBatch();
      		}
    	} catch (SQLException e) {
      		l.log(Level.SEVERE, "Failed to perform delete: "
        		+ ${object.name}SQL.DELETE_${relation_table_name?upper_case}_RELATION_BY_${object.name?upper_case}ID, e);
      		throw new PersistException("Failed to perform delete: "
        		+ ${object.name}SQL.DELETE_${relation_table_name?upper_case}_RELATION_BY_${object.name?upper_case}ID, e);
    	}
  	}

    <#if object.trashcan[0]?? && object.trashcan == "true">
  	/**
  	* Moves the relationship with ${att.@class} to the trash can. 
  	*/
  	protected void trashRelationWith${att.@class}(Long ${object.name?uncap_first}Id, Long ${att.@class?uncap_first}Id, boolean asBatch, boolean undo)
      	throws ConnectException, PersistException {
    	try {
      		makePreparedStatement(${object.name}SQL.TRASH_${relation_table_name?upper_case}_RELATION);
      		ps.setBoolean(1, !undo);
      		ps.setLong(2, ${object.name?uncap_first}Id);
      		ps.setLong(3, ${att.@class?uncap_first}Id);
      		if(!asBatch) {
        		ps.execute();
      		} else {
        		ps.addBatch();
      		}
    	} catch (SQLException e) {
      		l.log(Level.SEVERE, "Failed to perform trash: "
        		+ ${object.name}SQL.TRASH_${relation_table_name?upper_case}_RELATION, e);
      		throw new PersistException("Failed to perform trash: "
        		+ ${object.name}SQL.TRASH_${relation_table_name?upper_case}_RELATION, e);
    	}
  	}  

  	/**
  	* Moves all relationships with ${att.@class} to the trash can. 
  	*/
  	protected void trashAllRelationsWith${att.@class}(Long ${object.name?uncap_first}Id, boolean asBatch, boolean undo)
      	throws ConnectException, PersistException {
    	try {
      		makePreparedStatement(${object.name}SQL.TRASH_${relation_table_name?upper_case}_RELATION_BY_${object.name?upper_case}ID);
      		ps.setBoolean(1, !undo);
      		ps.setLong(2, ${object.name?uncap_first}Id);
      		if (!asBatch) {
        		ps.execute();
      		} else {
        		ps.addBatch();
      		}
    	} catch (SQLException e) {
      		l.log(Level.SEVERE, "Failed to perform trash: "
        		+ ${object.name}SQL.TRASH_${relation_table_name?upper_case}_RELATION_BY_${object.name?upper_case}ID, e);
      		throw new PersistException("Failed to perform trash: "
        		+ ${object.name}SQL.TRASH_${relation_table_name?upper_case}_RELATION_BY_${object.name?upper_case}ID, e);
    	}
  	}
    </#if>

  	/**
  	* Creates a relationship with {att.@class} by creating a record in the ${relation_table_name} table. 
  	*/
  	protected void connectTo${att.@class}(Long ${object.name?uncap_first}Id, Long ${att.@class?uncap_first}Id, boolean asBatch)
      	throws ConnectException, QueryException, CopyException, PersistException {
    	try {
    		//TODO this is wrong, the check if a connection exist must first be done in the PersistableSet and secend done with a select query
    
      		deleteRelationWith${att.@class}(${object.name?uncap_first}Id, ${att.@class?uncap_first}Id, asBatch);
      		makePreparedStatement(${object.name}SQL.INSERT_${relation_table_name?upper_case}_RELATION);
      		ps.setLong(1, ${object.name?uncap_first}Id);
      		ps.setLong(2, ${att.@class?uncap_first}Id);
      		if(!asBatch){
        		ps.execute();
      		} else {
        		ps.addBatch();
      		}
    	} catch (SQLException e) {
      		l.log(Level.SEVERE, "Failed to perform insert: "
        		+ ${object.name}SQL.INSERT_${relation_table_name?upper_case}_RELATION, e);
      		throw new PersistException("Failed to perform insert: "
        		+ ${object.name}SQL.INSERT_${relation_table_name?upper_case}_RELATION, e);
    	}
  	}
</#list>

  	/**
  	* Raw persist function. Persists the changes of ${object.name} to the database and
  	* also any changed related children.
  	* Business rules are not checked, that is why this method is private.
  	* @param asBatch Set to true if you want to execute the statements outside this method. You need to invoke ps.executeBatch() yourself.	
  	*/
  	protected Set<BusinessRuleViolation> persist(${object.name} pojo, boolean asBatch) throws ConnectException, PersistException, BusinessRuleViolationException, QueryException<#if object.attributes.set[0]??>, CopyException</#if> {
    	l.fine("Updating " + pojo.getId());

		Set<BusinessRuleViolation> retval = new HashSet<BusinessRuleViolation>();
    	String SQL = null;
    	try {
      		if (pojo.dirty.size() != 0) {
        		if (pojo.isNew) {
          			pojo.dirty.put(${object.name}Fields.creationDate, null);
          			pojo.creationDate = new Date((new Date().getTime()/1000)*1000); //Strip nanoseconds, they are not persisted in MySQL
          			StringBuilder sqlInsert = new StringBuilder("insert into ${object.name} (");
          			char comma = ' ';
          			for (${object.name}Fields field : pojo.dirty.keySet()) {
            			sqlInsert.append(comma);
            			sqlInsert.append(field.name());
            			comma = ',';
          			}
          			sqlInsert.append(") values (");
          			comma = ' ';
          			for (int i = 0; i < pojo.dirty.size(); i++) {
            			sqlInsert.append(comma);
            			sqlInsert.append('?');
            			comma = ',';
          			}
          			sqlInsert.append(')');
          			SQL = sqlInsert.toString();
        		} else {
          			if (pojo.dirty.size() > 0) {
            			pojo.dirty.put(${object.name}Fields.updateDate, pojo.updateDate);
            			pojo.updateDate = new Date((new Date().getTime()/1000)*1000); //Strip nanoseconds, they are not persisted in MySQL
            			StringBuilder sqlUpdate = new StringBuilder("update ${object.name} set ");
            			char comma = ' ';
            			for (${object.name}Fields field : pojo.dirty.keySet()) {
              				sqlUpdate.append(comma);
              				sqlUpdate.append(field.name()).append("=?");
              				comma = ',';
            			}
            		sqlUpdate.append(" where id=?");
            		SQL = sqlUpdate.toString();
          		}
        	}
        
        	makePreparedStatement(SQL);
        
        	int position = 1;
        	for (${object.name}Fields field : pojo.dirty.keySet()) {
          		switch (field) {
            		case updateDate:
              			ps.setTimestamp(position++, new Timestamp(pojo.updateDate.getTime()));
              			break;
            		case creationDate:
              			ps.setTimestamp(position++, new Timestamp(pojo.creationDate.getTime()));
              			break;
    <#list object.attributes?children as att>
        <#if att?node_name != "set" && att?node_name != "object" && att?node_type == "element">
            		case ${att.@fieldName}:
            <#if !att.@notnull[0]?? || att.@notnull == "false"> 
              			if (pojo.${att.@fieldName} == null) 
              				ps.setNull(position++, field.sqlType);
              			else
            </#if>
            <#if att?node_name="boolean">
                			ps.setBoolean(position++, pojo.${att.@fieldName}.booleanValue());
            <#elseif att?node_name == "date" || att?node_name == "datetime">
                			ps.setDate(position++, new java.sql.Date(pojo.${att.@fieldName}.getTime()));
            <#elseif att?node_name == "enum">
                			ps.setString(position++, pojo.${att.@fieldName}.name());
            <#elseif att?node_name == "integer">
                			ps.setInt(position++, pojo.${att.@fieldName}.intValue());
            <#elseif att?node_name == "long">
                			ps.setLong(position++, pojo.${att.@fieldName}.longValue());
            <#elseif att?node_name == "double">
                			ps.setDouble(position++, pojo.${att.@fieldName}.doubleValue());
            <#elseif att?node_name == "string" || att?node_name == "html">
                <#if att?node_name == "html" || att.@maxlength[0]?number &gt; 65000>
                			ps.setBinaryStream(position++, org.apache.commons.io.IOUtils.toInputStream(pojo.${att.@fieldName}));
                <#else>
                			ps.setString(position++, pojo.${att.@fieldName});
                </#if>
            <#elseif att?node_name == "xml"> 
                			ps.setBinaryStream(position++, util.marshall(pojo.${att.@fieldName}));
            <#else>
            				ERROR undefined type ${att?node_name}
            </#if>
              				break;
      </#if>
    </#list>
          			}
        		}
        
        		if (!pojo.isNew)
          			ps.setLong(position++, pojo.id.longValue());
          
        		if (!asBatch)
          			ps.execute();
        		else
          			ps.addBatch();
        
        		if (pojo.isNew && !asBatch)
          			pojo.id = ps.getLastInsertID();
        
        		pojo.clean();
      		}
    
    <#list object.attributes.set as att>
        <#if att.@multiplicity == "n-to-m">
      		if (!pojo.isNew) {
	    		for (${att.@class} delete : pojo.get${att.@plural?cap_first}().getRemovedObjects()) {
    	  			deleteRelationWith${att.@class}(pojo.getId(), delete.getId(), asBatch);
      			}
      		}
      		for (${att.@class} list : pojo.get${att.@plural?cap_first}()) {
        		if (list.isNew || list.isDirty()) {
          			ServiceFactory.${att.@class?uncap_first}Service().persist(list, asBatch);
        		}
        		connectTo${att.@class}(pojo.getId(), list.getId(), asBatch);
      		}
        <#else>
      		if (!pojo.isNew && pojo.get${att.@plural?cap_first}().getRemovedObjects().size() > 0) {
        		${att.@class}Service childService = ServiceFactory.${att.@class?uncap_first}Service();
        		for (${att.@class} delete : pojo.get${att.@plural?cap_first}().getRemovedObjects())
          			childService.delete(delete);
      		}
      		List<${att.@class}> update${att.@plural?cap_first} = new LinkedList<${att.@class}>();
      		for (${att.@class} child : pojo.get${att.@plural?cap_first}()) {
        		if (child.isDirty()) {
          			update${att.@plural?cap_first}.add(child);
          			child.set${object.name}Id(pojo.getId());
        		}
      		}
      		if (!update${att.@plural?cap_first}.isEmpty()) {
        		ServiceFactory.${att.@class?uncap_first}Service().persist(update${att.@plural?cap_first});
        		for (${att.@class} child : update${att.@plural?cap_first}) {
          			pojo.get${att.@plural?cap_first}().setMapKey(child);
        		}
      		}
        </#if>
      //TODO: re check this, it might be wrong...
      		if (!pojo.get${att.@plural?cap_first}().isRetrieved()) pojo.get${att.@plural?cap_first}().clear();
    </#list>
      		if (pojo.isNew) pojo.isNew = false;
   		} catch (MySQLIntegrityConstraintViolationException e) {
			String indexName = null;
			int start = e.toString().indexOf("for key '");
		
			if (start > 0) {
				indexName = e.toString().substring(start + 9);
				indexName = indexName.substring(0, indexName.indexOf("'"));
				retval.add(new BusinessRuleViolation(
					pojo, ${object.name}Fields.valueOf(uniqueKeyAttribute("${object.name}", indexName)), BusinessRuleViolation.StandardRule.UniqueKey.ruleId, pojo.getId() == null?BusinessRuleViolation.Action.Insert:BusinessRuleViolation.Action.Update)
				);
			} else {
				throw new PersistException("Failed to perform update: " + SQL, e);
			}
    	} catch(Exception e) {
      		l.log(Level.SEVERE, "Failed to perform update: " + SQL, e);
      		throw new PersistException("Failed to perform update: " + SQL, e);
    	}
    
    	return retval;
  	}

  	/**
  	* Deletes ${object.name}. 
  	* Collections associated to ${object.name} are deleted as well when cascade delete is indicated.
  	**/
  	public Set<BusinessRuleViolation> delete(${object.name} pojo) throws ConnectException, PersistException, BusinessRuleViolationException<#if object.attributes.set[0]??>, QueryException, CopyException</#if> {
    	Set<BusinessRuleViolation> br = checkBR(pojo, BusinessRuleViolation.Action.Delete);
    	if (br.size() == 0) {
      		delete(pojo, false);
    	}
    
    	return br;
  	}

  	/** Deletes ${object.name} without checking on business rule violations. */
  	private void delete(${object.name} pojo, boolean asBatch) throws ConnectException, PersistException {
    	l.fine("Deleting " + pojo.getId());
    
    	try {
<#list object.attributes.set as att>
    <#if att.@multiplicity[0]?? && att.@multiplicity == "n-to-m">
      		if (!pojo.get${att.@plural?cap_first}().isEmpty()) {
        		deleteAllRelationsWith${att.@class}(pojo.getId(), asBatch);
      		}
    <#elseif !att.@cascadeDelete[0]?? || att.@cascadeDelete == "true">
      		if (!pojo.get${att.@plural?cap_first}().isEmpty()) {
        		ServiceFactory.${att.@class?uncap_first}Service().deleteBy${object.name}Id(pojo.getId());
      		}
    </#if>
</#list>
      		makePreparedStatement(${object.name}SQL.DELETE);
      		ps.setLong(1, pojo.getId().longValue());
      		if (!asBatch)
        		ps.execute();
      		else
        		ps.addBatch();
    	} catch(Exception e) {
      		l.log(Level.SEVERE, "Failed to perform delete: " + ${object.name}SQL.DELETE, e);
      		throw new PersistException("Failed to perform delete: " + ${object.name}SQL.DELETE, e);
    	}
  	}

<#if object.trashcan[0]?? && object.trashcan == "true">
  	/**
  	* Finds trashed ${object.name}s. 
  	* @param ${object.name?uncap_first}Id (Optional) if set only a specific trashed object is retrieved.
  	**/
  	public List<${object.name}> findTrash(Long ${object.name?uncap_first}Id) throws ConnectException, QueryException, CopyException {
  		List<${object.name}> retVal = new ArrayList<${object.name}>();
    
    	try {
      		makePreparedStatement(${object.name?uncap_first}Id == null ? ${object.name}SQL.FIND_TRASH : ${object.name}SQL.FIND_TRASH_BY_ID);
      		if (${object.name?uncap_first}Id != null) {
        		ps.setLong(1, ${object.name?uncap_first}Id);
      		}
      		rs = ps.executeQuery();
      		while (rs.next()) {
        		retVal.add(util.copy(rs, 0, ${object.name?uncap_first}Id == null ? FetchScope.Table : FetchScope.Form));
      		}
      		rs.close();
      		rs = null;
    	} catch(SQLException e) {
      		l.log(Level.SEVERE, "Failed to find trash " + ${object.name?uncap_first}Id, e);
      		throw new QueryException("Failed to find trash " + ${object.name?uncap_first}Id, e);
    	}
    
    	return retVal;
  	}

  	/**
  	* Trashes ${object.name}. 
  	* Collections associated to ${object.name} are trashed as well when cascade delete is indicated.
  	**/
  	public Set<BusinessRuleViolation> trash(String undoHash, ${object.name} pojo) throws ConnectException, QueryException, PersistException, BusinessRuleViolationException<#if object.attributes.set[0]??>, QueryException, CopyException</#if> {
    	Set<BusinessRuleViolation> br = checkBR(pojo, BusinessRuleViolation.Action.Delete);
    	if (br.size() == 0) {
      		TrashService.trash(undoHash, pojo.getId(), "${object.name}");
      		trash(pojo, false, false);
    	}
    
    	return br;
  	}

  	/**
  	* Restored a trashed ${object.name} and also restored possible trashed children.
  	* @throws QueryException When ${object.name} was not found (or was untrashed before).
  	*/
  	public void undo(Long ${object.name?uncap_first}Id) throws ConnectException, QueryException, PersistException, BusinessRuleViolationException, CopyException {
    	List<${object.name}> trash = findTrash(${object.name?uncap_first}Id);
    
    	if (trash.isEmpty()) {
      		throw new QueryException("Could not find trashed ${object.name}, maybe it was already undeleted.");
    	}
    
    	${object.name} pojo = trash.iterator().next();
    	trash(pojo, false, true);
    	TrashService.deleteTrash(pojo.getId(), "${object.name}");
  	}

  	/** Trashes ${object.name} without checking on business rule violations. */
  	private void trash(${object.name} pojo, boolean asBatch, boolean undo)
      	throws ConnectException, QueryException, PersistException {
    	l.fine("Trashing " + pojo.getId());
    
    	try {
    <#list object.attributes.set as att>
        <#if att.@multiplicity[0]?? && att.@multiplicity == "n-to-m">
      		if (!pojo.get${att.@plural?cap_first}().isEmpty()) {
        		trashAllRelationsWith${att.@class}(pojo.getId(), asBatch, undo);
      		}
        <#elseif !att.@cascadeDelete[0]?? || att.@cascadeDelete == "true">
      		if (!pojo.get${att.@plural?cap_first}().isEmpty() || undo) {
        		ServiceFactory.${att.@class?uncap_first}Service().trashBy${object.name}Id(pojo.getId(), undo);
      		}
        </#if>
    </#list>
      		makePreparedStatement(${object.name}SQL.TRASH);
      		ps.setBoolean(1, !undo);
      		ps.setLong(2, pojo.getId().longValue());
      		if (!asBatch)
        		ps.execute();
      		else
        		ps.addBatch();
    	} catch(Exception e) {
      		l.log(Level.SEVERE, "Failed to perform trash: " + ${object.name}SQL.TRASH, e);
      		throw new PersistException("Failed to perform trash: " + ${object.name}SQL.TRASH, e);
    	}
  	}
</#if>
	
	/** Test - dumps findAll */
	public static void main(String[] args) throws ConnectException, QueryException, CopyException {
		for (${object.name} testRecord : ServiceFactory.${object.name?uncap_first}Service().find(${object.name}SQL.TEST, null, FetchScope.Table)) {
<#list object.tableScope?children as att>
	<#if att?node_name != "object" && att?node_type == "element">
			if (testRecord.${att.@name[0]!att.@class?uncap_first} != null)
				System.out.print(testRecord.${att.@name[0]!att.@class?uncap_first}.toString());
			else
				System.out.print("NULL");
			System.out.print("-");
	</#if>
</#list>
		System.out.println();
		}
	}
}
